Ontdek veilige cross-origin communicatie met de PostMessage API. Leer over de mogelijkheden, veiligheidsrisico's en best practices om kwetsbaarheden in webapplicaties te beperken.
Cross-Origin Communicatie: Beveiligingspatronen met de PostMessage API
In het moderne web moeten applicaties vaak interageren met bronnen van verschillende origins. Het Same-Origin Policy (SOP) is een cruciaal beveiligingsmechanisme dat scripts beperkt in de toegang tot bronnen van een andere origin. Er zijn echter legitieme scenario's waar cross-origin communicatie noodzakelijk is. De postMessage API biedt een gecontroleerd mechanisme om dit te bereiken, maar het is essentieel om de mogelijke beveiligingsrisico's te begrijpen en de juiste beveiligingspatronen te implementeren.
Het Same-Origin Policy (SOP) Begrijpen
Het Same-Origin Policy is een fundamenteel beveiligingsconcept in webbrowsers. Het beperkt webpagina's in het maken van verzoeken naar een ander domein dan het domein dat de webpagina heeft geleverd. Een 'origin' wordt gedefinieerd door het schema (protocol), de host (domein) en de poort. Als een van deze verschilt, worden de origins als verschillend beschouwd. Bijvoorbeeld:
https://example.comhttps://www.example.comhttp://example.comhttps://example.com:8080
Dit zijn allemaal verschillende origins, en het SOP beperkt directe scripttoegang tussen hen.
Introductie van de PostMessage API
De postMessage API biedt een veilig en gecontroleerd mechanisme voor cross-origin communicatie. Het stelt scripts in staat om berichten te sturen naar andere vensters (bijv. iframes, nieuwe vensters of tabbladen), ongeacht hun origin. Het ontvangende venster kan vervolgens luisteren naar deze berichten en ze dienovereenkomstig verwerken.
De basissyntaxis voor het verzenden van een bericht is:
otherWindow.postMessage(message, targetOrigin);
otherWindow: Een referentie naar het doelvenster (bijv.window.parent,iframe.contentWindow, of een vensterobject verkregen viawindow.open).message: De data die u wilt verzenden. Dit kan elk JavaScript-object zijn dat geserialiseerd kan worden (bijv. strings, getallen, objecten, arrays).targetOrigin: Specificeert de origin waarnaar u het bericht wilt verzenden. Dit is een cruciale beveiligingsparameter.
Aan de ontvangende kant moet u luisteren naar het message-event:
window.addEventListener('message', function(event) {
// ...
});
Het event-object bevat de volgende eigenschappen:
event.data: Het bericht dat door het andere venster is verzonden.event.origin: De origin van het venster dat het bericht heeft verzonden.event.source: Een referentie naar het venster dat het bericht heeft verzonden.
Beveiligingsrisico's en Kwetsbaarheden
Hoewel postMessage een manier biedt om SOP-beperkingen te omzeilen, introduceert het ook potentiële beveiligingsrisico's als het niet zorgvuldig wordt geïmplementeerd. Hier zijn enkele veelvoorkomende kwetsbaarheden:
1. Onjuiste Doel-Origin
Het niet valideren van de event.origin-eigenschap is een kritieke kwetsbaarheid. Als de ontvanger het bericht blindelings vertrouwt, kan elke website kwaadaardige data sturen. Verifieer altijd dat de event.origin overeenkomt met de verwachte origin voordat u het bericht verwerkt.
Voorbeeld (Kwetsbare Code):
window.addEventListener('message', function(event) {
// DOE DIT NIET!
processMessage(event.data);
});
Voorbeeld (Veilige Code):
window.addEventListener('message', function(event) {
if (event.origin !== 'https://trusted-origin.com') {
console.warn('Bericht ontvangen van niet-vertrouwde origin:', event.origin);
return;
}
processMessage(event.data);
});
2. Data-injectie
Het behandelen van de ontvangen data (event.data) als uitvoerbare code of het direct injecteren ervan in de DOM kan leiden tot Cross-Site Scripting (XSS) kwetsbaarheden. Sanitizeer en valideer altijd de ontvangen data voordat u deze gebruikt.
Voorbeeld (Kwetsbare Code):
window.addEventListener('message', function(event) {
if (event.origin === 'https://trusted-origin.com') {
document.body.innerHTML = event.data; // DOE DIT NIET!
}
});
Voorbeeld (Veilige Code):
window.addEventListener('message', function(event) {
if (event.origin === 'https://trusted-origin.com') {
const sanitizedData = sanitize(event.data); // Implementeer een correcte sanitization-functie
document.getElementById('message-container').textContent = sanitizedData;
}
});
function sanitize(data) {
// Implementeer hier robuuste sanitization-logica.
// Gebruik bijvoorbeeld DOMPurify of een vergelijkbare bibliotheek
return DOMPurify.sanitize(data);
}
3. Man-in-the-Middle (MITM) Aanvallen
Als communicatie via een onveilig kanaal (HTTP) plaatsvindt, kan een MITM-aanvaller de berichten onderscheppen en wijzigen. Gebruik altijd HTTPS voor veilige communicatie.
4. Cross-Site Request Forgery (CSRF)
Als de ontvanger acties uitvoert op basis van het ontvangen bericht zonder de juiste validatie, kan een aanvaller mogelijk berichten vervalsen om de ontvanger te verleiden tot het uitvoeren van onbedoelde acties. Implementeer CSRF-beschermingsmechanismen, zoals het opnemen van een geheim token in het bericht en het verifiëren ervan aan de ontvangende kant.
5. Gebruik van Wildcards in targetOrigin
Het instellen van targetOrigin op * staat elke origin toe om het bericht te ontvangen. Dit moet worden vermeden tenzij absoluut noodzakelijk, omdat het het doel van op origin gebaseerde beveiliging tenietdoet. Als u * moet gebruiken, zorg er dan voor dat u andere sterke beveiligingsmaatregelen implementeert, zoals message authentication codes (MAC's).
Voorbeeld (Vermijd Dit):
otherWindow.postMessage(message, '*'); // Vermijd het gebruik van '*' tenzij absoluut noodzakelijk
Beveiligingspatronen en Best Practices
Om de risico's die gepaard gaan met postMessage te beperken, volgt u deze beveiligingspatronen en best practices:
1. Strikte Validatie van de Origin
Valideer altijd de event.origin-eigenschap aan de ontvangende kant. Vergelijk deze met een vooraf gedefinieerde lijst van vertrouwde origins. Gebruik strikte gelijkheid (===) voor de vergelijking.
2. Dataschoning en Validatie
Maak alle data die via postMessage wordt ontvangen schoon en valideer deze voordat u ze gebruikt. Gebruik geschikte saneringstechnieken afhankelijk van hoe de data zal worden gebruikt (bijv. HTML-escaping, URL-encoding, invoervalidatie). Gebruik bibliotheken zoals DOMPurify voor het opschonen van HTML.
3. Message Authentication Codes (MACs)
Voeg een Message Authentication Code (MAC) toe aan het bericht om de integriteit en authenticiteit ervan te waarborgen. De zender berekent de MAC met een gedeelde geheime sleutel en voegt deze toe aan het bericht. De ontvanger herberekent de MAC met dezelfde gedeelde geheime sleutel en vergelijkt deze met de ontvangen MAC. Als ze overeenkomen, wordt het bericht als authentiek en ongewijzigd beschouwd.
Voorbeeld (met HMAC-SHA256):
// Zender
async function sendMessage(message, targetOrigin, sharedSecret) {
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify(message));
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(sharedSecret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
);
const signature = await crypto.subtle.sign("HMAC", key, data);
const signatureArray = Array.from(new Uint8Array(signature));
const signatureHex = signatureArray.map(b => b.toString(16).padStart(2, '0')).join('');
const securedMessage = {
data: message,
signature: signatureHex
};
otherWindow.postMessage(securedMessage, targetOrigin);
}
// Ontvanger
async function receiveMessage(event, sharedSecret) {
if (event.origin !== 'https://trusted-origin.com') {
console.warn('Bericht ontvangen van niet-vertrouwde origin:', event.origin);
return;
}
const securedMessage = event.data;
const message = securedMessage.data;
const receivedSignature = securedMessage.signature;
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify(message));
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(sharedSecret),
{ name: "HMAC", hash: "SHA-256" },
false,
["verify"]
);
const signature = await crypto.subtle.sign("HMAC", key, data);
const signatureArray = Array.from(new Uint8Array(signature));
const signatureHex = signatureArray.map(b => b.toString(16).padStart(2, '0')).join('');
if (signatureHex === receivedSignature) {
console.log('Bericht is authentiek!');
processMessage(message); // Ga door met het verwerken van het bericht
} else {
console.error('Verificatie van bericht-handtekening mislukt!');
}
}
Belangrijk: De gedeelde geheime sleutel moet veilig worden gegenereerd en opgeslagen. Vermijd het hardcoderen van de sleutel in de code.
4. Gebruik van Nonce en Tijdstempels
Om replay-aanvallen te voorkomen, voegt u een unieke nonce (eenmalig gebruikt getal) en een tijdstempel toe aan het bericht. De ontvanger kan dan verifiëren dat de nonce niet eerder is gebruikt en dat de tijdstempel binnen een acceptabel tijdsbestek valt. Dit beperkt het risico dat een aanvaller eerder onderschepte berichten opnieuw afspeelt.
5. Principe van de Minste Privileges
Verleen alleen de minimaal noodzakelijke privileges aan het andere venster. Als het andere venster bijvoorbeeld alleen data hoeft te lezen, sta dan niet toe dat het data schrijft. Ontwerp uw communicatieprotocol met het principe van de minste privileges in gedachten.
6. Content Security Policy (CSP)
Gebruik Content Security Policy (CSP) om te beperken uit welke bronnen scripts kunnen worden geladen en welke acties scripts kunnen uitvoeren. Dit kan helpen de impact van XSS-kwetsbaarheden te beperken die kunnen voortvloeien uit onjuiste afhandeling van postMessage-data.
7. Invoervalidatie
Valideer altijd de structuur en het formaat van de ontvangen data. Definieer een duidelijk berichtformaat en zorg ervoor dat de ontvangen data aan dit formaat voldoet. Dit helpt onverwacht gedrag en kwetsbaarheden te voorkomen.
8. Veilige Dataserialisatie
Gebruik een veilig dataserialisatieformaat, zoals JSON, om berichten te serialiseren en deserialiseren. Vermijd formaten die code-uitvoering toestaan, zoals eval() of Function().
9. Beperk de Berichtgrootte
Beperk de grootte van berichten die via postMessage worden verzonden. Grote berichten kunnen buitensporige middelen verbruiken en mogelijk leiden tot denial-of-service-aanvallen.
10. Regelmatige Beveiligingsaudits
Voer regelmatig beveiligingsaudits van uw code uit om potentiële kwetsbaarheden te identificeren en aan te pakken. Besteed bijzondere aandacht aan de implementatie van postMessage en zorg ervoor dat alle best practices op het gebied van beveiliging worden gevolgd.
Voorbeeldscenario: Veilige Communicatie Tussen een Iframe en de Bovenliggende Pagina
Overweeg een scenario waarbij een iframe gehost op https://iframe.example.com moet communiceren met de bovenliggende pagina gehost op https://parent.example.com. Het iframe moet gebruikersdata naar de bovenliggende pagina sturen voor verwerking.
Iframe (https://iframe.example.com):
// Genereer een gedeelde geheime sleutel (vervang door een veilige methode voor het genereren van sleutels)
const sharedSecret = 'UW_VEILIGE_GEDEELDE_SLEUTEL';
// Haal gebruikersdata op
const userData = {
name: 'John Doe',
email: 'john.doe@example.com'
};
// Verzend de gebruikersdata naar de bovenliggende pagina
async function sendUserData(userData) {
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify(userData));
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(sharedSecret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
);
const signature = await crypto.subtle.sign("HMAC", key, data);
const signatureArray = Array.from(new Uint8Array(signature));
const signatureHex = signatureArray.map(b => b.toString(16).padStart(2, '0')).join('');
const securedMessage = {
data: userData,
signature: signatureHex
};
parent.postMessage(securedMessage, 'https://parent.example.com');
}
sendUserData(userData);
Bovenliggende Pagina (https://parent.example.com):
// Gedeelde geheime sleutel (moet overeenkomen met de sleutel van het iframe)
const sharedSecret = 'UW_VEILIGE_GEDEELDE_SLEUTEL';
window.addEventListener('message', async function(event) {
if (event.origin !== 'https://iframe.example.com') {
console.warn('Bericht ontvangen van niet-vertrouwde origin:', event.origin);
return;
}
const securedMessage = event.data;
const userData = securedMessage.data;
const receivedSignature = securedMessage.signature;
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify(userData));
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(sharedSecret),
{ name: "HMAC", hash: "SHA-256" },
false,
["verify"]
);
const signature = await crypto.subtle.sign("HMAC", key, data);
const signatureArray = Array.from(new Uint8Array(signature));
const signatureHex = signatureArray.map(b => b.toString(16).padStart(2, '0')).join('');
if (signatureHex === receivedSignature) {
console.log('Bericht is authentiek!');
// Verwerk de gebruikersdata
console.log('Gebruikersdata:', userData);
} else {
console.error('Verificatie van bericht-handtekening mislukt!');
}
});
Belangrijke Opmerkingen:
- Vervang
UW_VEILIGE_GEDEELDE_SLEUTELdoor een veilig gegenereerde gedeelde geheime sleutel. - De gedeelde geheime sleutel moet hetzelfde zijn in zowel het iframe als de bovenliggende pagina.
- Dit voorbeeld gebruikt HMAC-SHA256 voor berichtauthenticatie.
Conclusie
De postMessage API is een krachtig hulpmiddel voor het mogelijk maken van cross-origin communicatie in webapplicaties. Het is echter cruciaal om de potentiële beveiligingsrisico's te begrijpen en geschikte beveiligingspatronen te implementeren om deze risico's te beperken. Door de beveiligingspatronen en best practices die in deze gids worden beschreven te volgen, kunt u postMessage veilig gebruiken om robuuste en veilige webapplicaties te bouwen.
Vergeet niet om altijd prioriteit te geven aan beveiliging en up-to-date te blijven met de nieuwste best practices voor webontwikkeling. Controleer regelmatig uw code en beveiligingsconfiguraties om ervoor te zorgen dat uw applicaties beschermd zijn tegen potentiële kwetsbaarheden.